﻿module HtmlDom

open Dom
open ScriptingExposedAttribute
open JsEngine
open JsTypes
open HtmlDocument
open StyledElements
open RenderingContext


type HTMLElement(doc:Document, tagName, attributes) =
    inherit StyledElement(doc, tagName, attributes)

    let _eventTracker = EventTracker()

    member this.ownerHtmlDocument with get() = doc :?> HTMLDocument

    // events
    abstract member defaultAction : Event->unit
    default this.defaultAction(ev) =
        match ev with
        | :? MouseEvent as mouseEvent when mouseEvent.``type``="click"  -> 
            if this.isFocusable
            then this.ownerHtmlDocument.setFocus(this)
        | _ -> ()

    override this.dispatchEvent (event:Event) =
        _eventTracker.invokeListeners event this.ownerHtmlDocument.jsEngine
        let invokeDefault =
            match this.parentNode with
            | :? HTMLElement as elem -> elem.dispatchEvent(event)
            | :? HTMLDocument as doc -> doc.dispatchEvent(event)
            | _ -> ()
        this.defaultAction(event)

    [<ScriptingExposed>]
    member this.addEventListener(name:string, handler:JsValue) =
        _eventTracker.addEventListener(name, handler)

    override this.isFocusable = false


type HTMLAnchorElement (doc, tagName, attributes) =
    inherit HTMLElement(doc, tagName, attributes)

    override this.defaultAction(e:Event) =
        match e with
        | :? MouseEvent as mouseEvent -> 
            let murl = this.attributes.TryFind "href"
            match murl with
            | Some url -> 
                let uri = this.ownerHtmlDocument.resolveUrl(url)
                this.ownerHtmlDocument.defaultView.navigate(uri)
            | None -> ()
        | _ -> ()

type IInputControl =
    abstract member getData : unit -> (string*string) option


type HTMLFormElement (doc, tagName, attributes) = 
    inherit HTMLElement(doc, tagName, attributes)   

    member this.getFormControls() =
        let rec getFormControls1 (root:Element) = 
            match box root with
            | :? IInputControl as input -> [input]
            | _ -> [for child in root.childElements do
                            yield! getFormControls1 child]
        getFormControls1 this
    
    member this.submit (submitBtn:Element) = 
        let controls = this.getFormControls()
        let data = [for ctrl in controls -> ctrl.getData()] |> List.choose id
        let url = this.attributeOrDefault("action", "")
        let httpMethod = this.attributeOrDefault("method", "get")
        let enctype = this.attributeOrDefault("enctype", "application/x-www-form-urlencoded")
        let uri = this.ownerHtmlDocument.resolveUrl(url)
        do this.ownerHtmlDocument.defaultView.submit(uri, httpMethod, enctype, data)


[<AbstractClass>]
type HTMLFormControlElement (doc, tagName, attributes) as this =
    inherit HTMLElement(doc, tagName, attributes)
    override this.isFocusable = true
    member this.submit (submitBtn:Element) = 
        let rec findForm1 (elem:ParentNode) =
            match elem.parentNode with
            | :? HTMLFormElement as form -> Some form
            | :? Element as parent -> findForm1 parent
            | _ -> None
        match findForm1 this with
        | Some form -> do form.submit submitBtn
        | _ -> ()

    member this.getData() =
            match this.attributes.TryFind "name" with
            | None -> None
            | Some name ->
                let value = this.getValue()
                Some (name, value)

    abstract member getValue :  unit -> string

    interface IInputControl with
        member z.getData() = this.getData()


type HTMLInputElement (doc, tagName, attributes) as this =
    inherit HTMLFormControlElement(doc, tagName, attributes)
    let _val = this.attributeOrDefault("value", "")
    let _child = doc.createTextNode(_val)
    do 
        // insert value as textnode    
        do this.appendChild(_child) |> ignore
    
    member this.valueChild = _child
    override this.getValue() =
        let value = this.valueChild.nodeValue
        value


[<ScriptingExposed>]
type HTMLInputTextElement (doc, tagName, attributes) =
    inherit HTMLInputElement(doc, tagName, attributes)
    override this.defaultAction(e:Event) =
        match e with
        | :? KeyboardEvent as keyboardEvent -> 
            // Add to text node
            this.valueChild.nodeValue <- this.valueChild.nodeValue + keyboardEvent.char.ToString()
            this.redraw()
        | _ -> ()
        base.defaultAction(e)

    [<ScriptingExposed>]
    member public this.value 
        with get() = 
            (this.childNodes.[0] :?> Text).nodeValue


[<ScriptingExposed>]
type HTMLTextareaElement (doc, tagName, attributes) =
    inherit HTMLFormControlElement(doc, tagName, attributes)

    member this.valueChild =
        if this.childNodes.Count = 0 
        then             
            // if element is empty, we create a text node
            let _child = doc.createTextNode("")
            do this.appendChild(_child) |> ignore

        (this.childNodes.[0] :?> Text)

    override this.defaultAction(e:Event) =
        match e with
        | :? KeyboardEvent as keyboardEvent -> 
            // Add to text node
            this.valueChild.nodeValue <- this.valueChild.nodeValue + keyboardEvent.char.ToString()
            this.redraw()
        | _ -> ()
        base.defaultAction(e)

    [<ScriptingExposed>]
    member public this.value 
        with get() = 
            (this.childNodes.[0] :?> Text).nodeValue

    override this.getValue() =
        this.value


[<ScriptingExposed>]
type HTMLInputSubmitElement (doc, tagName, attributes) =
    inherit HTMLInputElement(doc, tagName, attributes)
    override this.defaultAction(e:Event) =
        match e with
        | :? MouseEvent as mouseEvent when mouseEvent.``type``="click"  -> 
            do this.submit(this)
        | _ -> ()

[<ScriptingExposed>]
type HTMLButtonElement (doc, tagName, attributes) =
    inherit HTMLFormControlElement(doc, tagName, attributes)
    override this.defaultAction(e:Event) =
        if this.attributeOrDefault("type", "submit") = "submit"
        then
            match e with
            | :? MouseEvent as mouseEvent when mouseEvent.``type``="click"  -> 
                do this.submit()
            | _ -> ()

    override this.getValue() =
        let value = this.attributeOrDefault("value", "")
        value

type HTMLImgElement (doc:Document, tagName, attributes) as this =
    inherit HTMLElement(doc, tagName, attributes)
    do
        match attributes.TryFind "src" with
        | Some url -> 
            let uri = this.ownerHtmlDocument.resolveUrl url
            do this.ownerHtmlDocument.loader.fetchImage(uri, (fun img -> this.imageLoadet(img)))
        | _ -> ()

    [<DefaultValue>] val mutable imgData : System.Windows.Media.Imaging.BitmapSource option
    member this.imageLoadet(img) =
        do this.imgData <- Some img
        do this.redraw()


    interface IReplacedElement with
        member this.intrinsicSize() =
            match this.imgData with
            | None -> (0,0)
            | Some bitmap -> (bitmap.PixelWidth, bitmap.PixelHeight)
        member this.render (c:RenderingContext) =
            match this.imgData with
            | None -> ()
            | Some bitmap ->
                let rect = System.Windows.Rect(c.pos, System.Windows.Size(float(bitmap.PixelWidth), float(bitmap.PixelHeight)))
                do c.context.DrawImage(bitmap, rect)


type HTMLHtmlElement(doc, tagName, attributes, head:Element, body:Element) as this =
    inherit HTMLElement(doc, tagName, attributes)
    do        
        this.appendChild(head) |> ignore       
        this.appendChild(body) |> ignore
    member this.headElement = head
    member this.bodyElement = body


type HTMLStyleElement(doc, tagName, attributes) =
    inherit HTMLElement(doc, tagName, attributes)
    override this.onAdd(node) =
        match node with
        | :? Text as text ->
            this.ownerHtmlDocument.stylesheets.addCss(text.nodeValue)
        | _ -> ()

type HTMLScriptElement(doc, tagName, attributes) =
    inherit HTMLElement(doc, tagName, attributes)
    override this.onAdd(node) =
        match node with
        | :? Text as text ->
            do this.ownerHtmlDocument.jsEngine.eval(text.nodeValue) |> ignore
        | _ -> ()        

type HTMLBrElement(doc, tagName, attributes) as this =
    inherit HTMLElement(doc, tagName, attributes)
    do
        (* We insert a linebreak as text in the BR-element.
            This is a hack to support rendering of a line break,
            the cleaner solution is to use the css ::before pseudo element to insert the line break
        *)
        this.appendChild(doc.createTextNode("\n")) |> ignore

type Document with 
    member this.documentElement with get() = this.childElements.[0] :?> HTMLHtmlElement

let htmlElementFactory (doc:Document) (tagName:string) (attributes:Map<string,string>) =
    let elem =
        match tagName with
        |"a" -> HTMLAnchorElement(doc, tagName, attributes) :> HTMLElement
        |"br" -> HTMLBrElement(doc, tagName, attributes) :> HTMLElement
        |"img" -> HTMLImgElement(doc, tagName, attributes) :> HTMLElement
        |"form" -> HTMLFormElement(doc, tagName, attributes) :> HTMLElement
        |"button" -> HTMLButtonElement(doc, tagName, attributes) :> HTMLElement
        |"style" -> HTMLStyleElement(doc, tagName, attributes) :> HTMLElement
        |"script" -> HTMLScriptElement(doc, tagName, attributes) :> HTMLElement
        |"textarea" -> HTMLTextareaElement(doc, tagName, attributes) :> HTMLElement
        |"input" -> 
            let inputType = defaultArg (attributes.TryFind "type") "text"
            match inputType with
            | "submit" -> HTMLInputSubmitElement(doc, tagName, attributes) :> HTMLElement            
            | "text" | _ -> HTMLInputTextElement(doc, tagName, attributes) :> HTMLElement
        | _ -> HTMLElement(doc, tagName, attributes) 
    do doc.onAdd(elem)
    elem :> Element

